// This assumes that Prototype, the google maps API and jQuery have already been loaded...
var EnumRouteDataType = { KML:1, XML:2, JSON:3 };
var EnumJourneyType = { PLANE:1, CAR:2, BUS:3, TRAIN:4, BOAT:5, BIKE:6, MOTORBIKE:7 };
var KnownIcons = { "http://maps.gstatic.com/mapfiles/ms2/micons/rail.png": "TRAIN",
				   "http://maps.gstatic.com/intl/en_ALL/mapfiles/ms/micons/ferry.png": "BOAT",
				   "http://maps.gstatic.com/mapfiles/ms2/micons/cabs.png": "CAR",
				   "http://maps.gstatic.com/mapfiles/ms2/micons/bus.png": "BUS" };

// Extend the Google Maps API with a proper distance calculator function.				   
// Taken from http://groups.google.com/group/google-maps-js-api-v3/browse_thread/thread/3a9f3b83b941a2d7/06792073b2529413?lnk=gst&q=polyline+length#06792073b2529413
  google.maps.LatLng.prototype.kmTo = function(a){ 
    var e = Math, ra = e.PI/180; 
    var b = this.lat() * ra, c = a.lat() * ra, d = b - c; 
    var g = this.lng() * ra - a.lng() * ra; 
    var f = 2 * e.asin(e.sqrt(e.pow(e.sin(d/2), 2) + e.cos(b) * e.cos 
(c) * e.pow(e.sin(g/2), 2))); 
    return f * 6378.137; 
  } 
  
  google.maps.Polyline.prototype.inKm = function(n){ 
    var a = this.getPath(n), len = a.getLength(), dist = 0; 
    for(var i=0; i<len-1; i++){ 
      dist += a.getAt(i).kmTo(a.getAt(i+1)); 
    } 
    return dist; 
  } 

var RouteObject = function(routedata) {
	// Validate the mandatory parameters.
	if (isNaN(routedata.lat))
		throw ("RouteObject: lat is missing or not a number.");
	if (isNaN(routedata.lng))
		throw ("RouteObject: lng is missing or not a number.");
	if (isNaN(routedata.journeytype))
		throw ("RouteObject: journeytype is missing or not valid.");
	this.lat = routedata.lat; 
	this.lng = routedata.lng;
	this.title = routedata.title;
	// TODO: Verify type!
	this.type = routedata.journeytype;
	this.latlng = new google.maps.LatLng(this.lat, this.lng);
	this.previouslatlng = null;
	this.icon = routedata.icon;
	this.distance = 0;
	this.emissions = 0;
	this.rendered = false;

	// Set the directionmapping parameter based on the type of journey.
	switch (this.type)
	{
		case EnumJourneyType.TRAIN:
		case EnumJourneyType.BUS:
		case EnumJourneyType.CAR:
			this.directionmapping = true;
		break;
		
		default:
			this.directionmapping = false;
		break;
	}
	
	this.GetIcon = function()
	{
		return this.icon;
	};
	
	this.IsRendered = function()
	{
		return this.rendered;
	};
	
	this.GetDirectionMapping = function()
	{
		return this.directionmapping;
	};
	
	this.GetDirections = function()
	{
	};
	
	this.AwaitingDirections = function()
	{
		return this.awaitingdirections;
	};
	
	this.GetDirections = function()
	{
	};
	
	this.SetPreviousLatLng = function(previouslatlng)
	{
		this.previouslatlng = previouslatlng;
	};
	
	this.GetPreviousLatLng = function()
	{
		return this.previouslatlng;
	};
	
	// Gets the distance between this route marker and the last.
	// TODO: Complete this one.
	this.GetDistance = function()
	{
		return this.distance;
	};
	
	this.CalculateEmissions = function()
	{
		return 1000;
	};
	
	this.TooltipContent = function()
	{
		return this.description;
	};
};

var RouteList = function() {
	this.list = [];
	// In Google Maps V3 we need to also keep track of any map overlays in case we want to remove them later...
	// See https://developers.google.com/maps/documentation/javascript/overlays#RemovingOverlays
	this.overlays = [];
	this.PolyLineOptions = { strokeColor: "black", 
						     strokeOpacity: 1,
							 strokeWeight: 3,
							 clickable: true,
							 editable: false,
							 visible: true,
							 // Use a 'great circle' for accuracy.
							 geodesic: true };
							 
	this.carbonstats = { distance: 0,
						 totalemissions: 0,
						 distanceunits: "km",
						 emissionsunits: "g/CO2",
						 calculated: false
						};
	
	this.clear = function()
	{
		this.list = [];
		this.ClearOverlays();
		this.carbonstats.distance = 0;
		this.carbonstats.emissions = 0;
		this.carbonstats.calculated = false;
	};
	
	// Append a route object to the end of the list.
	this.append = function(routeobject)
	{
		if (this.list.length > 0)
		{
			routeobject.SetPreviousLatLng(this.list[this.list.length - 1].latlng);
		}
		this.list.push(routeobject);
	};
	
	// Run a function over each route object.
	this.each = function(func)
	{
		for (i = 0; i < this.list.length; i++)
		{
			var proxyfunc = jQuery.proxy(func, this);
			proxyfunc(i, this.list[i]);
		}
	};
	
	this.length = function()
	{
		return this.list.length;
	};
	
	// Check whether any of the objects in this list will need the directions service...
	this.RequiresDirections = function()
	{
		for (i = 0; i < this.list.length; i++)
		{
			if (this.list[i].directionmapping)
			{
				return true;
			}
		}
		
		return false;
	};
	
	this.SetMap = function(map)
	{
		this.googlemap = map;
	};
	
	this.GetRoute = function()
	{
		return this.list;
	};

	this.GetDistanceUnits = function()
	{
		return this.carbonstats.distanceunits;
	};
	
	this.GetTotalDistance = function()
	{
		// Function to calculate the entire distance of the route.
		if (!this.carbonstats.calculated)
			this.CalculateCarbonStats();

		return this.carbonstats.distance;
	};

	this.GetEmissionsUnits = function()
	{
		return this.carbonstats.emissionsunits;
	};

	this.GetTotalEmissions = function()
	{
		// Function to calculate the entire distance of the route.
		if (!this.carbonstats.calculated)
			this.CalculateCarbonStats();

		return this.carbonstats.emissions;
	};
	
	this.ClearOverlays = function()
	{
		// Set the map to null for each overlay and clear the array.
		while(this.overlays[0])
		{
			this.overlays.pop().setMap(null);
		}
	};
	
	this.RenderRoute = function()
	{
		// Check that we have a map to render to...
		if (this.googlemap == null)
		{
			throw("RouteList: No map to render to");
		}
		
		// Clear the old overlays.
		this.ClearOverlays();
		
		this.each(this.DrawRouteObject);
	};
	
	// We determine whether the route is rendered by checking each of the underlying journey objects.
	this.IsRendered = function()
	{
		// Empty lists cannot be classed as 'rendered'.
		if (this.list.length == 0) 
			return false;
	
		for (i = 0; i < this.list.length; i++)
		{
			if (!this.list[i].rendered)
			{
				return false;
			}
		}
		
		return true;
	};
	
	this.DrawRouteObject = function (index, routeobject)
	{	
		// If there are any locations that need looking up, do that here.
		if (routeobject.inexact)
		{
			// Todo: fill this in...
		}
		
		var marker = new google.maps.Marker({
			position: routeobject.latlng,
			map: this.googlemap,
			title: routeobject.title,
			icon: routeobject.icon,
			clickable: true
		});
		
		this.overlays.push(marker);
		
		var infowindow = new google.maps.InfoWindow({
			content: routeobject.title
		});
				
		google.maps.event.addListener(marker, 'click', function() {
		  infowindow.open(this.googlemap,marker);
		});

		// Map the directions for a more exact route if possible.
		if (routeobject.previouslatlng == null)
		{
			// We get the first one rendered for free (no previous marker points yet!)
			routeobject.rendered = true;
		}
		else
		{
			if (routeobject.directionmapping)
			{
				// TODO: Investigate sharing a service object for this...
				var directionsService = new google.maps.DirectionsService();
				var directionsDisplay = new google.maps.DirectionsRenderer();
				
				var request = {
					origin:  routeobject.GetPreviousLatLng(),
					destination: routeobject.latlng,
					travelMode: google.maps.TravelMode.DRIVING
				};
				
				var HandleDirections = function (result, status) 
				{
						if (status == google.maps.DirectionsStatus.OK) {
							directionsDisplay.setMap(this.googlemap);
							directionsDisplay.suppressMarkers = true;
							directionsDisplay.preserveViewport = true;
							directionsDisplay.polylineOptions = this.PolyLineOptions;
							directionsDisplay.setDirections(result);
							// The directions API gives a result in metres, but we store in km.
							routeobject.distance = result.routes[0].legs[0].distance.value/1000;
							this.overlays.push(directionsDisplay);
						}
						else
						{
							// Fall back to polyline rendering.
							routeobject.directionmapping = false;
							// Build the Polyline options 
							var pathOpts = { path: [ routeobject.latlng, routeobject.GetPreviousLatLng() ],
											 map: this.googlemap };
							var newPolyLineOptions = jQuery.extend(pathOpts, this.PolyLineOptions);
							var polyline = new google.maps.Polyline(newPolyLineOptions);
							routeobject.distance = polyline.inKm();
							
							this.overlays.push(polyline);
						}
						
						// Mark the route object as rendered.
						routeobject.rendered = true;
				};
				var ProxyHandleDirections = jQuery.proxy(HandleDirections, this);
				
				directionsService.route(request, ProxyHandleDirections);
			}
			
			// Use a second if statement to avoid code duplication here.
			if (!routeobject.directionmapping)
			{
				// Build the Polyline options 
				var pathOpts = { path: [ routeobject.latlng, routeobject.GetPreviousLatLng() ],
								 map: this.googlemap };
				var newPolyLineOptions = jQuery.extend(pathOpts, this.PolyLineOptions);
				var polyline = new google.maps.Polyline(newPolyLineOptions);
				routeobject.distance = polyline.inKm();
				
				this.overlays.push(polyline);
				// Mark the route object as rendered.
				routeobject.rendered = true;
			}
		}
	}
	
	/*
	* A note on units:
	* All distances are stored in kilometres.
	* All emissions are in grammes of CO2.
	*
	* Basis for the calculations:
	*
	*/
	this.CalculateCarbonStats = function()
	{
		// Work out the total carbon emissions for the journey as it stands at the moment.
		// And on a per-node basis.
		// Store these as an object attached to the route list.
		this.carbonstats.distance = 0;
		this.carbonstats.emissions = 0;
		
		var CalculateEmissionsPerRoute = function(index, routeobject)
		{
			this.carbonstats.distance += routeobject.distance;
			this.carbonstats.emissions += routeobject.CalculateEmissions();
		};
		
		this.each(CalculateEmissionsPerRoute);

		// Mark that the carbon stats have been calculated.
		this.carbonstats.calculated = true;
	};
};
	
var RoutemapCO2 = function (element, routefile, routefiletype) {
	this.IsSameDomain = function(url)
	{
		// Make any relative urls absolute.
		var element = document.createElement('span');
		var escapedurl = url.split('&').join('&amp;').split('"').join('&quot;').split('<').join('&lt;');
		element.innerHTML = '<a href="'+escapedurl+'">&nbsp;</a>';
		var absoluteurl = element.firstChild.href;
		
		// Check whether a request URL is from the same domain as this one.
		// TODO: This approach is pretty Mickey Mouse... work on a better solution.
		var thisdomain = document.URL.match(/:\/\/(www[0-9]?\.)?(.[^/:]+)/)[2];
		var thatdomain = absoluteurl.match(/:\/\/(www[0-9]?\.)?(.[^/:]+)/)[2];
		
		// Returns true if the domains are the same. False otherwise.
		return thisdomain == thatdomain;
	};
	
	this.GetDataLoadFunction = function(routefiletype)
	{
		var datafunction = null;
		
		// Delegate to the correct data loading function and set the data type.
		switch (routefiletype)
		{
			case EnumRouteDataType.KML:
				datafunction = "LoadDataKML";
			break;
			
			case EnumRouteDataType.XML:
				throw("RoutemapCO2: Raw XML Data not supported yet...");
			break;
			
			case EnumRouteDataType.JSON:
				throw("RoutemapCO2: JSON Data not supported yet...");
			break;
			
			default:
				throw("RoutemapCO2: Data type not supported... probably ever!");
			break;
		}
		
		return datafunction;
	};
	
	this.LoadData = function(newroutefile, newroutefiletype)
	{
		this.routefile = newroutefile;
		this.routefiletype = newroutefiletype;
	
		// Type checking...
		if (typeof(this.routefile) != "string")
			throw("RoutemapCO2: Route map is invalid");
	
		// Clear any existing markers...
		this.routelist.clear();
		
		// If the routefile argument is valid XML, try and parse it.
		try
		{
			jQuery.parseXML(this.routefile);
			// If we get this far, try and parse the data from here.
			
			var datafunctionname = this.GetDataLoadFunction(this.routefiletype);
			var datafunction = jQuery.proxy(this, datafunctionname);
			datafunction(this.routefile);
		}
		catch (err)
		{
			// Infer the route data type from the filename
			var jqdatatype = "xml";
			if (this.routefiletype == null)
			{
				var extension = this.routefile.toLowerCase().substring(this.routefile.length - 4, this.routefile.length - 1);
				switch (extension)
				{
					case "kml":
						this.routefiletype = EnumRouteDataType.KML;
						jqdatatype = "xml";
					break; 
					
					case "xml":
						this.routefiletype = EnumRouteDataType.XML;
						jqdatatype = "xml";
					break;

					case "json":
						this.routefiletype = EnumRouteDataType.JSON;
						jqdatatype = "json";
					break;

					default:
						throw("RoutemapCO2: Cannot work out the type of the map data file");
				}
			}
			
			var url = this.routefile;
			// Cross-Site fetching with ba-simple-proxy if required.
			if (!this.IsSameDomain(url))
			{
				this.crossdomain = true;
				url = "ba-simple-proxy.php?url="+escape(url)+"&mode=native";
			}
			else
			{
				this.crossdomain = false;
			}		
			
			// Call the loading function for the right data type and also provide cross-domain loading support above.
			var datafunction = this.GetDataLoadFunction(this.routefiletype);
			if (datafunction)
				jQuery.get(url, jQuery.proxy(this, datafunction), jqdatatype);
		}
	};
	
	this.LoadDataKML = function(data)
	{
		// Add all the Placemark objects to the routelist.
		var jQData = jQuery(data);
		// Assign a variable to allow the delegate to access our map's routelist...
		var routelist = this.routelist;
		routelist.clear();
		
		jQData.find("Placemark").each(function(index, placemark) {
			var jQPlacemark = jQuery(placemark);
			var routedata = {};
			
			var coordstring = jQPlacemark.find("Point coordinates").text();
			if (coordstring == null || coordstring.length == 0)
			{
				// Ignore anything except point co-ordinates for now.
			}
			else
			{	
				var coords = coordstring.split(",");
				routedata.lat = coords[1];
				routedata.lng = coords[0];
				routedata.title = jQPlacemark.find("name").text();
				routedata.description = jQPlacemark.find("description").text();
				var styleUrl = jQPlacemark.find("styleUrl").text();
				var style = jQData.find(styleUrl).first();
				var icon = style.find("Icon href").text();
				routedata.icon = icon;
				// Try to derive the transport type from the icon...
				// Look this up in the KnownIcons object, which can be added to!
				var journeytype = EnumJourneyType.PLANE;
				if (KnownIcons[icon] != null)
				{
					journeytype = eval("EnumJourneyType."+KnownIcons[icon]);
				}
				routedata.journeytype = journeytype;
				
				var routeobject = new RouteObject(routedata);
				routelist.append(routeobject);
			}
		});
		
		this.RenderMap();
	};
	
	this.SanityCheck = function()
	{
		// Check for common breakage and complain loudly.
		if (this.mapelement == null)
		{
			throw("RoutemapCO2: Can't find the map element to draw to");
		}
		
		if (this.calcelement == null)
		{
			throw("RoutemapCO2: Can't find the calculation element to draw to");
		}
		
		if (this.routelist == null)
		{
			throw("RoutemapCO2: Route List is undefined");
		}
	};
	
	this.ToggleEdit = function() 
	{
		this.editable = !this.editable;
		
	};
	
	this.Editable = function()
	{
		return this.editable;
	};
	
	this.ReadyToRender = function()
	{	
		this.SanityCheck();
		
		if (this.routelist.length == 0)
		{
			return false;
		}
		
		return true;
	};
	
	this.InitialiseMap = function()
	{
		var myOptions = {
			zoom: this.zoom,
			center: this.origin,
			mapTypeControl: true,
			mapTypeControlOptions: {style: google.maps.MapTypeControlStyle.DROPDOWN_MENU},
			navigationControl: true,
			navigationControlOptions: {style: google.maps.NavigationControlStyle.SMALL},
			mapTypeId: this.maptype
		};
		this.googlemap = new google.maps.Map(this.mapelement, myOptions);
	};
	
	this.RenderMap = function()
	{
		// Only render if we're good to go...
		if (this.ReadyToRender())
		{
			// Draw the route...
			this.routelist.SetMap(this.googlemap);
			this.routelist.RenderRoute();
			
			// Start drawing the carbon emissions panel...
			this.InitialiseCarbonPanel();
		}
	};
	
	this.InitialiseDirections = function()
	{
		// TODO: Maybe delete this?
	};
	
	this.DrawCarbonPanelButtons = function()
	{
		// Draw the buttons last.
		jQuery(this.calcelement).append("<div class='calcbuttons'>");
		var calcbuttons = jQuery(calcelement).find(".calcbuttons");
		// If the map is editable we'll need some more...
	};
	
	this.DrawCarbonPanel = function()
	{
		this.calcelement.innerHTML = "";

		// Distance information goes in one panel
		var distanceunits = "<span class='units'>"+this.routelist.GetDistanceUnits()+"</span>";
		var distanceinfo = "";
		distanceinfo += "<h3>Distances:</h3>";
		distanceinfo += "<ul>";
		distanceinfo += "<li><span class='title'>Total Distance:</span>";
		distanceinfo += "<span class='amount'>"+this.routelist.GetTotalDistance()+"</span></li>"; 
		distanceinfo += "</ul>"; 
		jQuery(this.calcelement).append("<div class='calcdistance'>" + distanceinfo + "</div>");
		
		// Emissions information goes in another.
		var emissionsunits = this.routelist.GetEmissionsUnits();
		var emissionsinfo = "";
		emissionsinfo += "<h3>Carbon Emissions</h3>";
		emissionsinfo += "<ul>";
		emissionsinfo += "<li><span class='title'>Total Distance:</span>";
		emissionsinfo += "<span class='amount'>"+this.routelist.GetTotalEmissions()+"</span></li>"; 
		emissionsinfo += "</ul>";
		jQuery(this.calcelement).append("<div class='calcemissions'>" + emissionsinfo + "</div>");
	};
	
	this.InitialiseCarbonPanel = function()
	{
		// First, check that we are ready to go with this...
		if (this.routelist.IsRendered())
		{
			this.routelist.CalculateCarbonStats();
			this.DrawCarbonPanel();
			// Animate the carbon panel first time.
			jQuery(this.calcelement).slideDown("slow");
		}
		else
			// This should run on a slow loop - trust that it'll eventually get there...
			window.setTimeout(jQuery.proxy(this.InitialiseCarbonPanel, this), 1000);
	};
	
	this.DrawEditPanel = function()
	{
	};
	
	/*
	*	Constructor begins here...
	*/
	// Initialise basic values.
	this.origin = new google.maps.LatLng(0, 10);
	this.mapelement = null;
	this.googlemap = null;
	this.calcelement = null;
	this.zoom = 2;
	this.maptype = google.maps.MapTypeId.ROADMAP;
	this.mapID = Math.floor(Math.random()*65535);
	this.routefile = routefile;
	this.routefiletype = routefiletype;
	this.routelist = new RouteList();

	// Set up internal divs for styling purposes.
	// TODO: Type check on the map element.
	// Pick a random ID for this map
	var routemapdivs = "<div class='routemapborder'>";
	routemapdivs += "<div class='routemap' id='routemap"+this.mapID+"' style='width:100%; height:100%'></div></div>";
	routemapdivs += "<div class='carboncalc' id='carboncalc"+this.mapID+"' style='width:100%; height:150px;'></div>";
	jQuery(element).append(routemapdivs);
	this.mapelement = document.getElementById("routemap"+this.mapID);
	this.calcelement = document.getElementById("carboncalc"+this.mapID);
	
	// Initialise and render the map...
	this.InitialiseMap();
	
	// Load the data directly if a file is provided...
	if (this.routefile)
		this.LoadData(this.routefile, this.routefiletype);
};
